/*
 * Copyright 2003-2007 (c) Live Media Pty Ltd. <argot@einet.com.au> 
 *
 * This software is licensed under the Argot Public License 
 * which may be found in the file LICENSE distributed 
 * with this software.
 *
 * More information about this license can be found at
 * http://www.einet.com.au/License
 * 
 * The Developer of this software is Live Media Pty Ltd,
 * PO Box 4591, Melbourne 3001, Australia.  The license is subject 
 * to the law of Victoria, Australia, and subject to exclusive 
 * jurisdiction of the Victorian courts.
 */

package com.colony.mime;

import java.util.Vector;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import java.nio.ByteBuffer;

/**
 *  The MimeByteBuffer provides a method of growing a data storage
 *  in chunks.  It provides both Input/Output streams.
 *
 *  The OutputStream can only be retrieved once.  After the output
 *  stream has been closed it can not be written to again.
 *
 *  The InputStream can be retrieved multiple times, however only after
 *  the output stream has been closed.
 */

public class MimeByteBuffer
{

	private static final int CHUNKSIZE = 2048;    
	private static Vector _freeChunks = new Vector();
	private static int _chunkCount = 0;
    
	public static synchronized ByteBuffer getChunk()
	{
		if ( _freeChunks.size() == 0 )
		{
			_chunkCount++;
			return ByteBuffer.allocateDirect( CHUNKSIZE );
		}
		
		ByteBuffer buffer = (ByteBuffer) _freeChunks.remove(0);
		
		// Reset the position to zero.
		buffer.position(0);
		
		return buffer;
	}

	public static synchronized void releaseChunk( ByteBuffer chunk )
	{
		_freeChunks.add( chunk );
	}

	public static int allocatedChunks()
	{
		return _chunkCount;
	}

	protected void finalize()
	{
		int len = _dataChunks.size();
		
		for ( int x = 0 ; x < len ; x++ )
		{
			releaseChunk( (ByteBuffer) _dataChunks.remove(0) );
		}
	}

	private Vector _dataChunks;
	private int _length;
	private boolean _closed;
	private boolean _gotOutputStream;

	/**
	 *  MimeByteBuffer
	 */

	public MimeByteBuffer()
	{
		_length = 0;
		_closed = false;
		_gotOutputStream = false;
		_dataChunks = new Vector();
		_dataChunks.add( getChunk() );	
	}

	/**
	 * Write a byte buffer to the chunkByteBuffer.
	 */

	protected void write( byte[] buffer, int offset, int length )
	throws IOException
	{
		if ( _closed == true ) 
			throw new IOException( "MimeByteBuffer Closed" );

		int chunkOffset = _length % CHUNKSIZE;
		ByteBuffer currentChunk = (ByteBuffer) _dataChunks.elementAt( _length / CHUNKSIZE );

      int toWrite = length;
      int written = 0;
 
		currentChunk.position( chunkOffset );

      while( toWrite > 0 )
      {
      	int w;
      	
      	// Will the data to write go past the end of the chunk?
      	if ( toWrite >= (CHUNKSIZE - chunkOffset))
      	{
      		w = (CHUNKSIZE - chunkOffset);

      		currentChunk.put( buffer, offset+written, w );
      		
      		written += w;
      		_length += w;
				toWrite -= w;
      		
      		currentChunk = getChunk();
      		_dataChunks.add( currentChunk );
      		chunkOffset = 0;
      	}
      	else
      	{
				w = toWrite;
				
      		currentChunk.put( buffer, offset+written, w );
      		
      		written += w;
      		_length += w;
				toWrite -= w;
      	}
      }

	}

	private void write( int ch )
	throws IOException
	{
		if ( _closed == true ) 
			throw new IOException( "MimeByteBuffer Closed" );

		int chunkOffset = _length % CHUNKSIZE;
		ByteBuffer currentChunk = (ByteBuffer) _dataChunks.elementAt( _length / CHUNKSIZE );

		currentChunk.put( chunkOffset, (byte) ch );

		_length++;
		chunkOffset++;

		if ( chunkOffset >= CHUNKSIZE )
		{
			currentChunk = getChunk();
			_dataChunks.add( currentChunk );
			chunkOffset = 0;
		}       
	}

	protected int read( int offset )
	{
		if ( offset >= _length ) return -1;  //EOF
		if ( offset < 0 ) return -1;        //EOF

		int chunkOffset = offset % CHUNKSIZE;
		ByteBuffer currentChunk = (ByteBuffer) _dataChunks.elementAt( offset / CHUNKSIZE );
		
		int value = currentChunk.get( chunkOffset );
		
		if ( value < 0 ) value = (value & 0x7F) + 128;

		return value;
	}


	protected int read( int offset, byte[] buffer, int pos, int length )
	throws IOException
	{
		if ( offset >= _length ) return -1;
		if ( offset < 0 ) throw new IOException( "Invalid Offset" );
		
		// crop the length to read if we would go over the end.
		if ( offset+length >= _length ) 
			length = _length - offset;
		
		int toRead = length;
		int read = pos;
		
		while ( toRead > 0 )
		{
			int chunkOffset = (offset+read) % CHUNKSIZE;
			ByteBuffer currentChunk = (ByteBuffer) _dataChunks.elementAt( (offset+read) / CHUNKSIZE );
			int r = 0;
			
			// Will our read take us past the end of the buffer
			if ( toRead > (CHUNKSIZE - chunkOffset) )
			{
				synchronized( currentChunk )
				{
					// read to the end of the buffer
					r = (CHUNKSIZE - chunkOffset);
					
					currentChunk.position( chunkOffset );
					currentChunk.get( buffer, pos+read, r );
					
					read += r;
					toRead -= r;
				}
			}
			// We're reading within the bufer.
			else
			{
				synchronized( currentChunk )
				{
					// read the last bytes.
					r = toRead;
					
					currentChunk.position( chunkOffset );
					currentChunk.get( buffer, pos+read, r );
					
					read+= r;
					toRead -= r;
				}
			}
			
		}
		
		return read;
	}

	
	
	private void close()
	{
		_closed = true;
	}


	public int length()
	{
		return _length;
	}

	public ChunkByteOutputStream getOutputStream()
	throws IOException
	{
		if ( _gotOutputStream == true ) 
			throw new IOException( "MimeByteBuffer OutputStream returned" );
		_gotOutputStream = true;
		return new ChunkByteOutputStream( this );
	}

	public class ChunkByteOutputStream
	extends OutputStream
	{
		private MimeByteBuffer _buffer;

		protected ChunkByteOutputStream( MimeByteBuffer buffer )
		{
			_buffer = buffer;
		}

		public void write( byte[] buffer, int offset, int length )
		throws IOException
		{
			_buffer.write( buffer, offset, length );
		}

		public void write( int b )
		throws IOException
		{
			_buffer.write( b );
		}

		public void close()
		throws IOException
		{
			_buffer.close();
		}

	}



	public ChunkByteInputStream getInputStream()
	throws IOException
	{
		if ( _closed == false ) 
			throw new IOException( "MimeByteBuffer Open" );
		return new ChunkByteInputStream( this );
	}
 
	public class ChunkByteInputStream 
	extends InputStream
	{
		private MimeByteBuffer _cbb;
		private int _offset;
		private int _markOffset;

		ChunkByteInputStream( MimeByteBuffer cbb )
		{
			_cbb = cbb;
			_offset = -1;  // Start before start of file.
			_markOffset = -2;
		}

		public void seek( int offset )
		{
			if ( offset < 0 ) _offset = 0;
			if ( offset > _cbb.length() ) _offset = _cbb.length();
			_offset = offset;
		}

		public int offset()
		{
	   	return _offset;
		}

		public int read()
		{
			if ( _offset > _cbb.length() ) return -1;
			_offset = _offset+1;
			return _cbb.read( _offset );
		}
		
		public int read( byte[] buffer, int pos, int length )
		throws IOException
		{
			_offset++;
			int r = _cbb.read( _offset, buffer, pos, length );
			if ( r > 0 )
				_offset += (r-1);
			return r;
		}

		public void reset()
		throws IOException
		{
			if ( _markOffset >= -1 )
			{
				_offset = _markOffset;
				_markOffset = -2;
			}
			else
				throw new IOException( "Mark not set" );
		}

		public void mark(int readlimit)
		{
			_markOffset = _offset;       
		}

		public boolean markSupported()
		{
			return true;
		}

		public String readLine()
		{
			StringBuffer buffer = new StringBuffer();
			int oldOffset = _offset;
			int ch;
			boolean cr = false;

			while ( true )
			{
				ch = read();
				if ( ch == -1 ) break;  // EOF Found
				if ( ch == 13 )   // Found CR
					cr = true;
				else if ( ch == 10 && cr == true )  // Found LF following CR.
		   		break;
				else
				{
		   		cr = false;
					buffer.append( (char) ch );
				}
			}

			if ( oldOffset == _offset ) return null;   // End of file Found.
			return buffer.toString();
		}
	}

}